﻿package com.zouloux.core.data.parse
{
	import com.zouloux.core.data.IDataItem;
	import com.zouloux.core.data.parse.IDataParser;
	import com.zouloux.core.debug.Debug;
	import com.zouloux.utils.ArrayUtils;
	
	/**
	 * ...
	 * @author ZoulouX
	 */
	public class DataParser implements IDataParser
	{
		/**
		 * Les items à parser
		 */
		protected var _items					:Array					= [];
		
		/**
		 * Parser de manière stricte
		 */
		protected var _strict					:Boolean				= false;
		
		/**
		 * Récupérer la listes des items à parser
		 */
		public function get items ():Array { return _items; }
		public function set items (value:Array):void 
		{
			_items = value;
		}
		
		/**
		 * Parser de manière stricte
		 */
		public function get strict():Boolean { return _strict; }
		public function set strict(value:Boolean):void 
		{
			_strict = value;
		}
		
		
		/**
		 * Le constructeur,
		 * le DataParser permet de convertir des données brutes en DataItems compatibles avec les DataCollection
		 */
		public function DataParser ()
		{
		}
		
		/**
		 * Ajouter un type d'item à parser
		 * @param	pItemName : Le nom (exemple "News")
		 * @param	pItemClass : La classe associée (exemple NewsItem)
		 */
		public function addItemType (pItemName:String, pItemClass:Class):void 
		{
			_items[pItemName] = pItemClass;
		}
		
		/**
		 * Effacer un type d'item
		 */
		public function removeItemType (pItemClass:Class):void
		{
			_items = ArrayUtils.deleteElement(_items, pItemClass);
		}
		
		/**
		 * Permet de parser le résultat d'une requête.
		 * Les types ajouté par les méthodes addParseType seront automatiquement castés lorsqu'un objet du même nom sera rencontré.
		 * Attention, cette méthode est récursive, il est conseillé de l'utiliser dans un bloc de test try catch.
		 * @param	pData : Les données brutes, peut provenir d'un service JSON AMF ou XML
		 * @return : Les données parsées avec les items
		 */
		public function parse (pData:*, pClass:Class = null):*
		{
			// Si c'est de l'XML, on prépare pour le parse
			if (pData is XML)
			{
				// Vérifier si cet XML n'a pas de mappage
				if (pClass == null)
				{
					// La liste des objets
					var listOut:Array = [];
					
					// Parcourir les élément de l'XML
					for each (var xmlItem:XML in (pData as XML).elements())
					{
						// L'objet contenant l'item
						var obj:Object = { };
						
						// Ce noeud n'est pas encore vérifié
						var verifiedNode:Boolean = false;
						
						// Parcourir la liste des items
						for (var k:* in _items) 
						{
							// Vérifier si un élément XML porte le même nom qu'un item
							if (k == xmlItem.name())
							{
								// Parser l'objet et lui attribuer un nom
								obj[xmlItem.name()] = parse(xmlItem, _items[k]);
								
								// Ce noeud à été vérifié
								verifiedNode = true;
								
								// Casser
								break;
							}
						}
						
						// S'il n'a pas été vérifier, chercher plus en profondeur
						if (!verifiedNode)
							obj[xmlItem.name()] = parse(xmlItem);
						
						// Ajouter ce noeud au tableau
						listOut.push(obj);
					}
					
					// Retourner la liste
					return listOut;
				}
				else
				{
					// Créer l'objet mappé
					var outItemXML:IDataItem = new pClass () as IDataItem;
					
					// Récupérer tous les attributs
					for each (var attribute:XML in (pData as XML).attributes())
					{
						// Récupérer le nom de l'attribut
						var attributeName:String = attribute.name().toString();
						
						// Parser cet attribut et le caster dans l'item
						outItemXML[attributeName] = parseByType(outItemXML[attributeName], attribute.toString());
					}
					
					// Récupérer tous les noeuds
					for each (var nodeElement:XML in (pData as XML).elements())
					{
						// Si le noeud est complèxe c'est sûrement un objet
						if (nodeElement.hasComplexContent())
						{
							// Ce noeud n'est pas encore vérifié
							var verifiedXmlNode:Boolean = false;
							
							// Parcourir la liste des items
							for (var m:* in _items) 
							{
								// Vérifier si un élément XML porte le même nom qu'un item
								if (m == xmlItem.name())
								{
									// Ajouter et parser ce noeud
									outItemXML[nodeElement.name()] = parse(outItemXML[nodeElement.name()], _items[m]);
									
									// Ce noeud à été vérifié
									verifiedXmlNode = true;
									
									// Casser
									break;
								}
							}
							
							// S'il n'a pas été vérifier, chercher plus en profondeur
							if (!verifiedXmlNode)
								outItemXML[nodeElement.name()] = parseByType(outItemXML[nodeElement.name()], nodeElement.toString());
						}
						else
							outItemXML[nodeElement.name()] = parseByType(outItemXML[nodeElement.name()], nodeElement.toString());
					}
					
					// Retourner l'item
					return outItemXML;
				}
			}
			else if (typeof(pData) == "object")
			{
				// Créer l'objet de sortie
				var out:*;
				
				// Vérifier si on doit mapper avec une classe
				if (pData is Array)
					out = [ ];
				else if (pClass != null)
					out = new pClass () as IDataItem;
				else
					out = { };
				
				// Parcourir l'objet à mapper
				for (var i:* in pData)
				{
					// Vérifier le type de chaque child
					var verified:Boolean = false;
					for (var j:* in _items)
					{
						// Vérifier si cet item est à parser
						if (i == j)
						{
							// Essayer
							try
							{
								// Parser en passant la classe de l'item
								out[i] = parse(pData[i], _items[j]);
								
								// Ce child est vérifié
								verified = true;
							}
							catch (e:Error)
							{
								// Formater le message d'erreur
								var errorMessage:String = "Remote parse error {In " + j + ", " + e.errorID + ": " + e.message + ", " + e.name + "} "+e.getStackTrace();
								
								// Signaler au debug
								Debug.core(this, errorMessage);
								
								// Si c'est en mode stricte, on lève une erreur et on arrête le massacre
								if (_strict)
									throw new Error(errorMessage);
							}
							
							// Casser
							break;
						}
					}
					
					// Si ce child n'a pas été vérifier, le parser sans classmap
					if (!verified)
					{
						// Vérifier si c'est un DataItem
						if (out is Array)
							out.push(parse(pData[i]));
						if (out is IDataItem)
							out[i] = parseByType(out[i], pData[i]);
						else
							out[i] = parse(pData[i]);
					}
				}
				
				// Retourner l'objet de retour \o/
				return out;
			}
			else if (pClass == Number || pClass == uint || pClass == int)
			{
				// Parser le number
				return parseFloat(pData);
			}
			else if (pClass == Date)
			{
				// Créer l'objet date
				var date:Date = new Date();
				
				// Remettre la date à 0 (pour date heure etc)
				date.setTime(0);
				
				// Vérifier le type de date
				if (/([0-9]{2,4})-([0-9]{1,2})-([0-9]{1,2})/.test(pData))
				{
					// Splitter la chaine de caractères
					var splited:Array = pData.toString().split("-");
					
					// Vérifier le type de l'année, 1900 ou 2000
					if (splited[0].length < 4)
						splited[0] = (parseFloat(splited[0]) >= 70 ? "19" : "20") + splited[0];
					
					// Spécifier la date
					date.setFullYear(parseInt(splited[0], 10));
					date.setMonth(parseInt(splited[1], 10) - 1);
					date.setDate(parseInt(splited[2], 10));
				}
				else
				{
					// Sinon format timestamp
					date.setTime(parseFloat(pData) * 1000);
				}
				
				// Retourner la date
				return date;
			}
			else if (pClass == Boolean)
			{
				// Parser le boolean
				return (pData.toString().toLowerCase() == "true" || pData == true || pData == "1")
			}
			else return pData; // retourner simplement la donnée si ce n'est pas un objet
		}
		
		/**
		 * Permet de parser un objet selon le type d'un autre objet.
		 * Cette méthode permet de convertir chaque variable d'un item dans son type de base.
		 * @param	pValue : La valeur de base à observer pour le type
		 * @param	pToParse : La valeur à convertir
		 * @return : L'objet converti
		 */
		protected function parseByType (pValue:*, pToParse:*):Object
		{
			// Vérifier le type de pValue, parser pToParse selon le type
			if (pValue is Array)
				return parse(pToParse, Array);
			if (pValue is uint || pValue is int || pValue is Number)
				return parse(pToParse, Number);
			else if (pValue is Date)
				return parse(pToParse, Date);
			else if (pValue is Boolean)
				return parse(pToParse, Boolean);
			else
				return parse(pToParse);
		}
	}
}